Skip to content

inject() should accept bound variables - #3636

Closed
danielbodart wants to merge 1 commit into
apache:masterfrom
danielbodart:inject-generic-argument-varargs
Closed

inject() should accept bound variables#3636
danielbodart wants to merge 1 commit into
apache:masterfrom
danielbodart:inject-generic-argument-varargs

Conversation

@danielbodart

@danielbodart danielbodart commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What

inject() is the only variadic-generic step in the Gremlin grammar that rejects a bound variable. This changes both inject productions from genericLiteralVarargs to genericArgumentVarargs, so inject(<variable>) parses and resolves like every sibling step.

Why (consistency)

A …Argument rule is exactly literal | variable — the variable alternative is the bound parameter. Counting who uses which variadic-generic rule in Gremlin.g4:

  • genericArgumentVarargs (accepts a bound variable) is used by V(), E() (spawn and mid-traversal), hasId(), hasValue(), property(), and — the closest structural twin — within() / without().
  • genericLiteralVarargs (literal-only) is referenced by nothing but the two inject productions.

So inject is the lone holdout: its nearest sibling within(), which does the identical "spray a list of values in" job, already accepts a bound parameter. This aligns inject with the rest of the language rather than adding anything new to it.

Compatibility

Strict superset. genericArgument includes genericLiteral (maps included), so every existing inject(...) call parses and behaves exactly as before; only the previously-rejected inject(<variable>) becomes valid. Verified against the existing negative grammar corpus (incorrect-gremlin-values.txt) — it contains no bare identifiers, so nothing moves from "correctly rejected" to "now accepted."

Changes

  • Grammar (Gremlin.g4): both inject productions → genericArgumentVarargs.
  • Reference visitors (TraversalSourceSpawnMethodVisitor, TraversalMethodVisitor): route through ArgumentVisitor.parseObjectVarargs(ctx.genericArgumentVarargs()), identical to how V()/E() already work.
  • Groovy translators (Java + JavaScript): the inject(x, null) Groovy-closure disambiguation walked a genericLiteralExpr layer that genericArgumentVarargs doesn't have (the args sit directly under the varargs node); adapted to the flatter shape. Output is unchanged for all existing cases including inject(1, null).
  • Tests: grammar-level parse cases for g.inject(x) / g.V().inject(x) (BasicGrammarTest), and an end-to-end variable-resolution assertion in GremlinQueryParserTest.shouldParseVariablesInVarargs mirroring the existing g.V(x, y, 300) case.
  • CHANGELOG entry.

The now-orphaned genericLiteralVarargs rule is left in place (harmless; its context class is still generated, keeping GenericLiteralVisitor/DefaultGremlinBaseVisitor compiling) — happy to remove it if preferred.

Notes

  • No JIRA filed yet — glad to open one if that's preferred for tracking.
  • Generated ANTLR parsers (all language targets) are build-time artifacts and will regenerate from the grammar; no committed generated code changed.

🤖 Generated with Claude Code

@codecov-commenter

codecov-commenter commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.67%. Comparing base (a28cd1f) to head (d45b885).
⚠️ Report is 594 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3636      +/-   ##
============================================
+ Coverage     76.35%   76.67%   +0.31%     
- Complexity    13424    14328     +904     
============================================
  Files          1012     1037      +25     
  Lines         60341    64759    +4418     
  Branches       7075     7694     +619     
============================================
+ Hits          46076    49656    +3580     
- Misses        11548    12010     +462     
- Partials       2717     3093     +376     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@danielbodart
danielbodart force-pushed the inject-generic-argument-varargs branch 2 times, most recently from 98d4d2e to 37a4162 Compare September 2, 2026 06:33
@danielbodart

Copy link
Copy Markdown
Contributor Author

How do you get code coverage to rerun?

inject() was the only variadic-generic step whose grammar used
genericLiteralVarargs rather than genericArgumentVarargs, so it rejected
a bound variable that every sibling step (V, E, within, without, hasId,
hasValue, property) already accepts. Change both inject productions to
genericArgumentVarargs and route the two inject visitors through
ArgumentVisitor.parseObjectVarargs, mirroring V()/E(). Adapt the Groovy
translators (Java and JavaScript) to the flatter varargs tree shape.

This is a strict superset: genericArgument includes genericLiteral, so
every existing inject(...) call is unchanged and only inject(<variable>)
becomes newly valid.
@danielbodart
danielbodart force-pushed the inject-generic-argument-varargs branch from 37a4162 to d45b885 Compare September 2, 2026 06:50
@spmallette

Copy link
Copy Markdown
Contributor

hello and thanks for the contribution. i was wondering if you had a specific need/use case for this feature that you could share or if it was just a point of syntax consistency that was driving it. any details you could share with us on that?

@danielbodart

danielbodart commented Sep 10, 2026 via email

Copy link
Copy Markdown
Contributor Author

@Cole-Greer

Copy link
Copy Markdown
Contributor

Hi @danielbodart, thanks for the submission and the response. Your federated queries use case sounds quite interesting, although there may be a connection there that I am missing. I'm not quite understanding why the map cannot be inlined into the InjectStep as a genericMapLiteral instead of passing via a variable. I'll share some context below on reasonings behind literal/argument divide with steps in the first place, but in short I'm not understanding the need for map variables in inject() at this time.

The entire purpose of passing argument's to steps instead of literal's is to take advantage of query caching mechanisms which providers may optionally implement. The intent is that the raw query string (including the variable name) can be used as a key for this cache, and then providers can swap in specific parameter values later upon cache hits. Unless a provider leveraging such a caching strategy, there's generally not any advantage to passing step arguments as a variable instead of directly in-lining it as a literal.

With this in mind, supporting variables in a step is actually a much more involved process than simply enabling the functionality in the grammar and parser. There needs to be an ability for the variables to be preserved inside the parsed GraphTraversal, and those variables must be able to survive optimizing TraversalStrategy application. The established pattern for this is to box variables into a GValue, which gets passed into a special GValueHolder placeholder step. You can reference a class like GraphStepPlaceholder as an example.

The preservation of GValues in the parser is controlled by the VariableResolver which is configured. We typically use DirectVariableResolver for most of our testing, which immediately reduces all variables and replaces them with their literals. In this configuration, there is no meaningful distinction between an argument being passed as a variable or a literal. If you were to instead configure DefaultVariableResolver, you would find that these variables will instead manifest as GValue objects in the parsed GraphTraversal, which would break the semantics of InjectStep without further modifications to follow the GValueHolder pattern. As-is, this would result in a InjectStep<GValue<Map>> instead of an InjectStep<Map> as desired. A GValue should never survive to the point of traversal execution.

The set of steps which are currently permitted to accept variables was carefully curated to target steps which showed the greatest need and upside for this query caching use case. Due to the complexity of the GValueHolder pattern, we deliberately withheld such behaviour from steps which we did not find justified the complexity.

@danielbodart

danielbodart commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Interesting, so the reason I can't inline them is because the values from the parent traversal can contain whole sub graphs of data that are projected into the sibling graph (so can be pretty big / unbounded) and exactly as you say I can now benefit from query caching. My implementation uses SQLlitre as the backing store and any bindings just become prepare statement bindings 1-2-1 with no extra complexity introduced. So inlining (for my implementation) can actually stop the query running (I lower the whole gremlin traversal into a relational algebra and then compile to SQL) and with unbound inlining of a graph that could easily hit the max statement size of SQLlite, it's not cachable as you already mentioned but it's also a lot slower to parse as the map would need to be parsed by gremlin parser (antlr-ng in my case) rather than as native code inside SQLite.

But I am a realist and understand none of these are your concerns! I didn't realise the inconsistency was intentional and though this was an easy win for everyone and just wanted to upstream my patch. I can carry on shipping an enhanced gremlin library for the federated use cases. I'll close this but have a few more upstream patches I'd like to see if any of them are interest to you all

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants